From: David Polakovic Date: Sun, 8 Mar 2026 13:31:20 +0000 (+0100) Subject: feat: Initial commit, with two base scripts. X-Git-Tag: 1.0.0 X-Git-Url: https://git.dpolakovic.space/%22%22.esc_url%28%24base_url%29.%22//%22%22/%22%22.esc_url%28%24base_url%29.%22//%22%22?a=commitdiff_plain;p=git-server-helpers feat: Initial commit, with two base scripts. --- 248c072cc4d2640da2f22ef4577df3a9ce12a08c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84e4199 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +TO_DO.txt \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..10bc13c --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,127 @@ +The "Artistic License" + + Preamble + +The intent of this document is to state the conditions under which a +Package may be copied, such that the Copyright Holder maintains some +semblance of artistic control over the development of the package, +while giving the users of the package the right to use and distribute +the Package in a more-or-less customary fashion, plus the right to make +reasonable modifications. + +Definitions: + + "Package" refers to the collection of files distributed by the + Copyright Holder, and derivatives of that collection of files + created through textual modification. + + "Standard Version" refers to such a Package if it has not been + modified, or has been modified in accordance with the wishes + of the Copyright Holder as specified below. + + "Copyright Holder" is whoever is named in the copyright or + copyrights for the package. + + "You" is you, if you're thinking about copying or distributing + this Package. + + "Reasonable copying fee" is whatever you can justify on the + basis of media cost, duplication charges, time of people involved, + and so on. (You will not be required to justify it to the + Copyright Holder, but only to the computing community at large + as a market that must bear the fee.) + + "Freely Available" means that no fee is charged for the item + itself, though there may be fees involved in handling the item. + It also means that recipients of the item may redistribute it + under the same conditions they received it. + +1. You may make and give away verbatim copies of the source form of the +Standard Version of this Package without restriction, provided that you +duplicate all of the original copyright notices and associated disclaimers. + +2. You may apply bug fixes, portability fixes and other modifications +derived from the Public Domain or from the Copyright Holder. A Package +modified in such a way shall still be considered the Standard Version. + +3. You may otherwise modify your copy of this Package in any way, provided +that you insert a prominent notice in each changed file stating how and +when you changed that file, and provided that you do at least ONE of the +following: + + a) place your modifications in the Public Domain or otherwise make them + Freely Available, such as by posting said modifications to Usenet or + an equivalent medium, or placing the modifications on a major archive + site such as uunet.uu.net, or by allowing the Copyright Holder to include + your modifications in the Standard Version of the Package. + + b) use the modified Package only within your corporation or organization. + + c) rename any non-standard executables so the names do not conflict + with standard executables, which must also be provided, and provide + a separate manual page for each non-standard executable that clearly + documents how it differs from the Standard Version. + + d) make other distribution arrangements with the Copyright Holder. + +4. You may distribute the programs of this Package in object code or +executable form, provided that you do at least ONE of the following: + + a) distribute a Standard Version of the executables and library files, + together with instructions (in the manual page or equivalent) on where + to get the Standard Version. + + b) accompany the distribution with the machine-readable source of + the Package with your modifications. + + c) give non-standard executables non-standard names, and clearly + document the differences in manual pages (or equivalent), together + with instructions on where to get the Standard Version. + + d) make other distribution arrangements with the Copyright Holder. + +5. You may charge a reasonable copying fee for any distribution of this +Package. You may charge any fee you choose for support of this +Package. You may not charge a fee for this Package itself. However, +you may distribute this Package in aggregate with other (possibly +commercial) programs as part of a larger (possibly commercial) software +distribution provided that you do not advertise this Package as a +product of your own. You may embed this Package's interpreter within +an executable of yours (by linking); this shall be construed as a mere +form of aggregation, provided that the complete Standard Version of the +interpreter is so embedded. + +6. The scripts and library files supplied as input to or produced as +output from the programs of this Package do not automatically fall +under the copyright of this Package, but belong to whoever generated +them, and may be sold commercially, and may be aggregated with this +Package. If such scripts or library files are aggregated with this +Package via the so-called "undump" or "unexec" methods of producing a +binary executable image, then distribution of such an image shall +neither be construed as a distribution of this Package nor shall it +fall under the restrictions of Paragraphs 3 and 4, provided that you do +not represent such an executable image as a Standard Version of this +Package. + +7. C subroutines (or comparably compiled subroutines in other +languages) supplied by you and linked into this Package in order to +emulate subroutines and variables of the language defined by this +Package shall not be considered part of this Package, but are the +equivalent of input as in Paragraph 6, provided these subroutines do +not change the language in any way that would cause it to fail the +regression tests for the language. + +8. Aggregation of this Package with a commercial distribution is always +permitted provided that the use of this Package is embedded; that is, +when no overt attempt is made to make this Package's interfaces visible +to the end user of the commercial distribution. Such use shall not be +construed as a distribution of this Package. + +9. The name of the Copyright Holder may not be used to endorse or promote +products derived from this software without specific prior written permission. + +10. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + + The End \ No newline at end of file diff --git a/add-contributor-wizard.pl b/add-contributor-wizard.pl new file mode 100644 index 0000000..c6891d6 --- /dev/null +++ b/add-contributor-wizard.pl @@ -0,0 +1,125 @@ +#!/usr/bin/perl +use strict; +use warnings; + +# Check if running as root +if ($< != 0) { + die "Error: This script must be run as root (use sudo)\n"; +} + +# Repository base path +my $REPO_BASE = '/path/to/your/repos'; + +# ============================================================ +# MAIN SCRIPT +# ============================================================ + +print "===========================================\n"; +print " Add Contributor to Repository\n"; +print "===========================================\n\n"; + +# List available repositories +opendir(my $dh, $REPO_BASE) or die "Error: Cannot open $REPO_BASE: $!\n"; +my @repos = grep { -d "$REPO_BASE/$_" && $_ ne '.' && $_ ne '..' } readdir($dh); +closedir($dh); + +if (@repos == 0) { + die "Error: No repositories found in $REPO_BASE\n"; +} + +# Display repositories +print "Available repositories:\n"; +my $count = 1; +my %repo_map; +foreach my $repo (sort @repos) { + print " $count. $repo\n"; + $repo_map{$count} = $repo; + $count++; +} + +# Get repository selection +print "\nEnter repository number: "; +chomp(my $repo_num = ); + +if (!exists $repo_map{$repo_num}) { + die "Error: Invalid repository number.\n"; +} + +my $selected_repo = $repo_map{$repo_num}; +my $repo_dir = "$REPO_BASE/$selected_repo"; +my $hook_file = "$repo_dir/hooks/update"; + +# Check if hook file exists +if (!-f $hook_file) { + die "Error: Update hook not found at $hook_file\n"; +} + +# Get contributor username +print "Enter contributor username (e.g., john.doe): "; +chomp(my $contributor = ); + +if ($contributor eq '') { + die "Error: Contributor username cannot be empty.\n"; +} + +# Sanitize username +$contributor =~ s/[^a-zA-Z0-9._-]//g; + +if ($contributor eq '') { + die "Error: Invalid contributor username.\n"; +} + +# Read current hook file +open(my $fh, '<', $hook_file) or die "Error: Cannot read $hook_file: $!\n"; +my @lines = <$fh>; +close($fh); + +# Find and modify the ALLOWED_CONTRIBUTORS line +my $found = 0; +my $already_exists = 0; + +for (my $i = 0; $i < @lines; $i++) { + if ($lines[$i] =~ /^ALLOWED_CONTRIBUTORS="(.+)"/) { + my $current_contributors = $1; + + # Check if contributor already exists + if ($current_contributors =~ /\b\Q$contributor\E\b/) { + $already_exists = 1; + last; + } + + # Add new contributor + $lines[$i] = "ALLOWED_CONTRIBUTORS=\"$current_contributors $contributor\"\n"; + $found = 1; + last; + } +} + +if ($already_exists) { + print "\nContributor '$contributor' is already authorized for $selected_repo\n"; + exit 0; +} + +if (!$found) { + die "Error: Could not find ALLOWED_CONTRIBUTORS line in hook file.\n"; +} + +# Write modified hook file +open(my $out_fh, '>', $hook_file) or die "Error: Cannot write to $hook_file: $!\n"; +print $out_fh @lines; +close($out_fh); + +# Ensure hook is executable +chmod 0755, $hook_file; + +print "\n===========================================\n"; +print " Contributor Added Successfully!\n"; +print "===========================================\n"; +print "Repository: $selected_repo\n"; +print "Contributor: $contributor\n"; +print "===========================================\n"; +print "\n$contributor can now push to feature branches in $selected_repo\n"; +print "Only owner can push to master branch.\n"; +print "===========================================\n"; + +exit 0; diff --git a/new-project-wizard.pl b/new-project-wizard.pl new file mode 100644 index 0000000..e5122fc --- /dev/null +++ b/new-project-wizard.pl @@ -0,0 +1,425 @@ +#!/usr/bin/perl +use strict; +use warnings; +use File::Path qw(make_path remove_tree); +use File::Spec; +use Cwd qw(abs_path); + +# ============================================================ +# CONFIGURATION CONSTANTS - Edit these as needed +# ============================================================ + +# Repository base path - where all repositories will be created +use constant REPO_BASE => '/path/to/your/repos'; + +# Master branch user - who can push to master +use constant MASTER_BRANCH_USER => 'YOUR-GIT-USERNAME-HERE'; + +# ============================================================ +# Check if running as root +# ============================================================ + +if ($< != 0) { + die "Error: This script must be run as root (use sudo)\n"; +} + +# ============================================================ +# TEMPLATES +# ============================================================ + +my $README_TEMPLATE = <<'END_README'; + +

HELLO WORLD

+ +END_README + +my $CONFIG_JS_TEMPLATE = <<'END_JS'; +END_JS + +my $HOOK_UPDATE_TEMPLATE = <<'END_HOOK'; +#!/bin/bash + +# Branch protection hook +# Controls who can push to master and who can push to the repository at all + +refname="$1" +oldrev="$2" +newrev="$3" + +# Get the user who is pushing (set by SSH authorized_keys) +PUSHER="${USER}" + +# Define the protected branch +PROTECTED_BRANCH="refs/heads/master" + +# Define allowed user for master branch +ALLOWED_MASTER_USER="__MASTER_USER__" + +# Define allowed contributors (space-separated list) +# Edit this list to add/remove contributors for THIS repository +ALLOWED_CONTRIBUTORS="__MASTER_USER__" + +# Check if user is allowed to push to this repository at all +if [[ ! " ${ALLOWED_CONTRIBUTORS} " =~ " ${PUSHER} " ]]; then + echo "===============================================" + echo "ERROR: You are not authorized to push to this repository." + echo "===============================================" + echo "" + echo "You are authenticated as: $PUSHER" + echo "" + echo "Allowed contributors: $ALLOWED_CONTRIBUTORS" + echo "" + echo "Please contact the repository owner for access." + echo "===============================================" + exit 1 +fi + +# Check if pushing to protected branch +if [ "$refname" = "$PROTECTED_BRANCH" ]; then + # Check if pusher is the allowed user + if [ "$PUSHER" != "$ALLOWED_MASTER_USER" ]; then + echo "===============================================" + echo "ERROR: Only $ALLOWED_MASTER_USER can push to master branch." + echo "===============================================" + echo "" + echo "You are authenticated as: $PUSHER" + echo "" + echo "Please:" + echo " 1. Push to a feature branch instead:" + echo " git push origin your-feature-branch" + echo "" + echo " 2. Request a merge to master via email" + echo "===============================================" + exit 1 + fi +fi + +# Allow the push +exit 0 +END_HOOK + +# ============================================================ +# HELPER FUNCTIONS +# ============================================================ + +# Sanitize and validate project name +sub validate_project_name { + my ($name) = @_; + + # Check if empty + if (!defined $name || $name eq '') { + return (0, "Project name cannot be empty"); + } + + # Check length + if (length($name) > 100) { + return (0, "Project name too long (max 100 characters)"); + } + + # Check for invalid characters + if ($name =~ m{[^a-zA-Z0-9_\-]}) { + return (0, "Project name can only contain letters, numbers, hyphens, and underscores"); + } + + # Check for path traversal attempts + if ($name =~ /\.\./ || $name =~ m{/} || $name =~ m{\\}) { + return (0, "Project name cannot contain '..' or path separators"); + } + + # Check for reserved git names + my @reserved = qw(.git hooks objects refs info config description HEAD); + if (grep { lc($name) eq lc($_) } @reserved) { + return (0, "Project name '$name' is reserved by git"); + } + + return (1, $name); +} + +# Safely execute a command with proper escaping +sub safe_system { + my @args = @_; + my $result = system(@args); + return ($result == 0); +} + +# Safely create a file with content +sub safe_write_file { + my ($filepath, $content) = @_; + + open(my $fh, '>', $filepath) or return 0; + print $fh $content; + close($fh) or return 0; + + return 1; +} + +# Cleanup function for partial repository +sub cleanup_repo { + my ($repo_dir) = @_; + + if (-d $repo_dir) { + print "Cleaning up partial repository...\n"; + remove_tree($repo_dir, {error => \my $err}); + if (@$err) { + warn "Warning: Some cleanup errors occurred\n"; + } + } +} + +# ============================================================ +# MAIN SCRIPT +# ============================================================ + +print "===========================================\n"; +print " Git Repository Creation Wizard\n"; +print "===========================================\n\n"; + +# Verify REPO_BASE exists and is accessible +my $repo_base = REPO_BASE; +if (!-d $repo_base) { + die "Error: Repository base directory '$repo_base' does not exist.\n"; +} +if (!-w $repo_base) { + die "Error: Repository base directory '$repo_base' is not writable.\n"; +} + +# Get and validate project name +my $project_name; +my $repo_dir; +while (1) { + print "Enter project name: "; + chomp($project_name = ); + + my ($valid, $result) = validate_project_name($project_name); + if (!$valid) { + print "Error: $result\n"; + print "Please try again.\n\n"; + next; + } + + $project_name = $result; + $repo_dir = File::Spec->catdir($repo_base, $project_name); + + # Check if repository already exists + if (-e $repo_dir) { + print "Error: Repository '$project_name' already exists.\n"; + print "Please choose a different name.\n\n"; + next; + } + + last; +} + +# Get project description +print "Enter project description: "; +chomp(my $description = ); +if ($description eq '') { + $description = "No description provided"; +} + +# Get category +# these are categories that I use, you can setup your own... +my %CATEGORIES = ( + 1 => '1 - programs', + 2 => '2 - scripts', + 3 => '3 - other', +); + +print "\nSelect category:\n"; +foreach my $key (sort keys %CATEGORIES) { + print " $key - $CATEGORIES{$key}\n"; +} + +my $category; +while (1) { + print "Enter category number: "; + chomp(my $category_num = ); + + if (!exists $CATEGORIES{$category_num}) { + print "Error: Invalid category number. Please try again.\n\n"; + next; + } + + $category = $CATEGORIES{$category_num}; + + last; +} + +# Get owner name +print "Enter owner name: "; +chomp(my $owner = ); +if ($owner eq '') { + $owner = "Unknown"; +} + +## NOTE: language and license are not standard git information I use +## them on my git server, but you can comment them out or just skip +## during the wizards run. + +# Get license +print "Enter license (e.g., MIT, GPL, Apache): "; +chomp(my $license = ); +if ($license eq '') { + $license = "Unknown"; +} + +# Get programming language +print "Enter primary language (e.g., Perl, Python, JavaScript): "; +chomp(my $language = ); +if ($language eq '') { + $language = "Unknown"; +} + +# Ask if repository is public (default: yes) +print "Is the repository public? (Y/n): "; +chomp(my $is_public = ); +$is_public = lc($is_public); + +# Default to public if empty or yes +my $public = ($is_public eq '' || $is_public eq 'y' || $is_public eq 'yes') ? 1 : 0; + +# Confirm creation +print "\n===========================================\n"; +print " Repository Summary\n"; +print "===========================================\n"; +print "Name: $project_name\n"; +print "Description: $description\n"; +print "Category: $category\n"; +print "Owner: $owner\n"; +print "License: $license\n"; +print "Language: $language\n"; +print "Visibility: " . ($public ? "Public" : "Private") . "\n"; +print "Path: $repo_dir\n"; +print "===========================================\n"; +print "Create this repository? (yes/no): "; +chomp(my $confirm = ); + +if (lc($confirm) ne 'yes' && lc($confirm) ne 'y') { + print "Repository creation cancelled.\n"; + exit 0; +} + +# Create repository with error handling +print "\nCreating repository...\n"; + +eval { + # Create directory + if (!safe_system('mkdir', '-p', $repo_dir)) { + die "Cannot create directory $repo_dir"; + } + + # Change to repository directory + chdir($repo_dir) or die "Cannot change to directory $repo_dir: $!"; + + # Initialize bare repository + if (!safe_system('git', 'init', '--bare', '--shared')) { + die "git init failed"; + } + + # Create description file + if (!safe_write_file('description', $description)) { + die "Cannot create description file"; + } + + # Create git-daemon-export-ok if public + if ($public) { + if (!safe_write_file('git-daemon-export-ok', '')) { + die "Cannot create git-daemon-export-ok"; + } + } + + # Create category file + if (!safe_write_file('category', $category)) { + die "Cannot create category file"; + } + + # Set owner using git config + if (!safe_system('git', 'config', '-f', 'config', 'gitweb.owner', $owner)) { + die "Failed to set owner"; + } + + # Set license using git config + if (!safe_system('git', 'config', '-f', 'config', 'gitweb.license', $license)) { + die "Failed to set license"; + } + + # Set language using git config + if (!safe_system('git', 'config', '-f', 'config', 'gitweb.language', $language)) { + die "Failed to set language"; + } + + # Add to safe directories + if (!safe_system('git', 'config', '--global', '--add', 'safe.directory', $repo_dir)) { + warn "Warning: Failed to add to safe directories\n"; + } + + # Create branch protection hook with username substitution + my $hook_content = $HOOK_UPDATE_TEMPLATE; + my $master_user = MASTER_BRANCH_USER; + $hook_content =~ s/__MASTER_USER__/$master_user/g; + + if (!safe_write_file('hooks/update', $hook_content)) { + die "Cannot create update hook"; + } + + # Make hook executable + if (!chmod(0755, 'hooks/update')) { + die "Cannot make update hook executable"; + } + + # Create custom-summary directory + if (!make_path("custom-summary")) { + die "Cannot create custom-summary directory"; + } + + # Create README.html + if (!safe_write_file('README.html', $README_TEMPLATE)) { + die "Cannot create README.html"; + } + + # Create config.js + if (!safe_write_file('custom-summary/config.js', $CONFIG_JS_TEMPLATE)) { + die "Cannot create config.js"; + } + + # Set ownership to git user + if (!safe_system('chown', '-R', 'git:git', $repo_dir)) { + die "Failed to set ownership"; + } + + # Set permissions + if (!safe_system('chmod', '-R', '755', $repo_dir)) { + die "Failed to set permissions"; + } +}; + +# Handle errors with cleanup +if ($@) { + my $error = $@; + print "\n===========================================\n"; + print " ERROR: Repository Creation Failed\n"; + print "===========================================\n"; + print "Error: $error\n"; + + cleanup_repo($repo_dir); + + print "===========================================\n"; + exit 1; +} + +# Success message +print "\n===========================================\n"; +print " Repository Created Successfully!\n"; +print "===========================================\n"; +print "Repository: $project_name\n"; +print "Location: $repo_dir\n"; +print "Visibility: " . ($public ? "Public" : "Private") . "\n"; +print "\nYou can now clone it with:\n"; +print " git clone git\@git.dpolakovic.space:$project_name\n"; +if ($public) { + print "\nOr publicly via:\n"; + print " git clone git://git.dpolakovic.space/$project_name\n"; +} +print "===========================================\n"; + +exit 0;